告警分级怎么做?一个实用的分类框架 原创
本文是 SRE 实战系列第 13 篇,聚焦告警分级这一个具体问题:怎么定级、怎么让级别自动化、怎么让级别长期不腐化。
和同系列三篇文章的分工:
- 第 6 篇《告警治理》解决"告警太多"——砍噪音、合并、燃烧率替换阈值
- 第 8 篇《on-call 生存指南》给出三级划分的结论(P0/P1/P2)和路由配置
- 本文解决"级别从哪来、谁来定、怎么保证不腐化"——这是前面两篇没展开、但真正决定落地成败的一环
一、先说结论:分级不是给告警打标签,是给组织签协议
1.1 现状:四种典型的分级失败
下面四种情况,我在不同团队里都见过。如果你的团队命中两条以上,这篇文章值得读完。
| 症状 | 现场长什么样 | 根因 |
|---|---|---|
| 没有级别 | 100 条告警全部 severity: warning,全走同一个钉钉群 | 从没定义过级别 |
| 级别里带数字 | P0 P1 P2 P3 P4,外加 critical/high/mid/low 两套并存 | 分级维度和优先级混在一起 |
| 级别靠人填 | 每个开发提告警规则时凭感觉选,同一条 SQL 慢查询在三个服务里是三个级别 | 定级标准不在规则里,在人脑里 |
| 级别熵增 | 半年前 90% 是 P2,现在一半告警是 P0 | 没有审计和降级机制,级别只升不降 |
1.2 一个判断标准
判断一次分级是不是做对了,不看文档写得多漂亮,看一句话:
凌晨 3 点,值班工程师被叫醒。他能不能在不看系统、不翻群的前提下回答:我现在最应该做的那一个动作是什么?
如果他能,分级是有效的;如果他醒来第一件事是打开仪表盘"看看是什么情况"——那说明这条告警本来就不该叫醒他。
分级最终落地的形态,是一个动作,不是一个数字。
1.3 分级的本质是签协议
这点必须先说,否则后面所有配置都会被稀释成形式主义。
在 SRE 体系里,一个告警级别意味着三件事同时达成:
- 对业务:这一类事件,我们承诺在 X 时间内开始处理,Y 时间内给出结论
- 对团队:收到这个级别,值班人有权力中断手上一切工作(包括打断别人的会)
- 对人:这个级别可以响铃、可以打电话、可以半夜叫醒人
这三条里最贵的是第三条。组织里最稀缺的资源不是服务器、不是带宽,而是人的注意力。级别就是注意力的配额。你每抬一次级,就是在从全团队的可支配注意力池里再借一笔;借多了,池子就崩了(也就是第 6 篇讲的告警疲劳)。
所以,一条原则贯穿全文:
级别不是"这个问题有多严重",是"我们要求多少人、多快、用什么方式被卷进来"。
只做前者叫描述,做到后者才叫分级。
二、分类框架:四个维度,五个级别
市面上的分级方案大多只有一个维度(严重程度),这一个维度承载不了真实决策。我用的框架是四个维度:
2.1 维度一:用户感知(Impact)——定级的主轴
这一栏,永远是定级的第一决定因素,而不是指标有多吓人。
怎么判断"用户感知"?不要拍脑袋,回到 SLI(第 3 篇)。三条可判定的事实:
- 有没有真实用户受影响:错误率、可用性是否真的破了 SLO 阈值(不是"5xx 数量看起来有点多")
- 影响面有多大:全量 / 单地域 / 单租户 / 内部用户 / 仅监控可见、体验无感
- 是否正在扩大:还在恶化、已平稳、已在自愈
一个真实的反差:CPU 打到 95% 但错误率是 0,用户无感,这是 P3;CPU 只有 60% 但支付成功率从 99.9% 掉到 97%,这是 P1。指标是症状,SLO 是事实。
2.2 维度二:紧迫性(Urgency)——决定"什么时候处理"
用户受影响 ≠ 现在必须处理。紧迫性回答的是"能不能等到明天早上":
- 正在恶化、且没有自愈机制兜底 → 高紧迫性
- 已经平稳在坏的水平,但不再变差 → 低紧迫性(可以排进当天工单)
- 趋势性劣化(磁盘 7 天写满、证书 14 天过期) → 计划性处理
这一维度是把"严重"和"紧急"拆开的关键。很多团队分级失效,就是把这两个维度压成了一根轴。
2.3 维度三:可自动化(Automatability)——决定"要不要人介入"
这条经常被忽略,但它能砍掉大量级别误判。先问一句:这个故障能不能由系统自己止血?
- 能自愈且已验证(Pod 重启、HPA 扩容、熔断降级、流量切换)→ 降级或转通知,人不需要被叫醒
- 能自愈但没验证过(写了重启策略但从没触发过)→ 仍然按人介入定级,同时把它列为待演练项
- 完全不能自愈(数据损坏、配置错误、依赖方挂掉)→ 按影响 + 紧迫性定级
第 8 篇讲过自动止血。这里的实践是:凡是能被可靠自动处理的告警,级别一律降到不响铃的那一档。不会因为"我们做了自愈"就没人知道,它仍然会出现在看板和日报里,只是不占用半夜的注意力。
2.4 维度四:责任边界(Ownership)——决定"叫醒谁"
级别和责任人绑定。同一条告警,落在不同团队手上,级别可能不同:
| 场景 | SRE / 平台团队 | 业务研发团队 |
|---|---|---|
| 数据库主从延迟 30s | P1(有全局影响) | P2(可用只读降级撑住) |
| 单服务发布后错误率上升 | P3(观察) | P1(该回滚就回滚) |
| 集群节点 NotReady | P1 | P3(等平台处理) |
同一条告警只归属一个 owner。如果要通知两个团队,用路由抄送,不要复制告警。归属不清的告警,是级别腐化的最大来源——两边都以为是对方的 P1,最后谁都不是。
2.5 五级对照表
四个维度组合起来,落到五个级别。这张表建议直接抄进团队的告警规范:
| 级别 | 谁被卷进来 | 通知方式 | 响应 SLA | 用户感知 | 紧迫性 | 典型例子 |
|---|---|---|---|---|---|---|
| P0 | 值班 + 二线 + 业务负责人 + 值班经理 | 电话 + 短信 + 群 | 5 分钟内响应,15 分钟出结论 | 核心链路全挂、数据丢失或损坏 | 正在恶化且无兜底 | 支付全链路 5xx、主库宕机、订单库数据损坏 |
| P1 | 值班 + 服务 owner | 电话或 IM 强提醒 | 30 分钟内响应 | 核心链路部分受损 / 非核心全挂 | 可能恶化 | 单地域故障、成功率跌破 SLO 燃烧率阈值 |
| P2 | 服务 owner | IM,仅工作时间 | 当日处理 | 有用户抱怨但可绕过 | 平稳的坏 | 非核心服务降级、某个 App 版本崩溃率升高 |
| P3 | 服务 owner | 看板 + 周报 | 本周内处理 | 用户无感 | 趋势性 | 容量水位、性能劣化趋势、单实例异常 |
| P4 | 无人 | 只入库 | 不要求 | 无 | 无 | 用于统计与审计,不对外通知 |
和第 8 篇的三级方案怎么衔接:第 8 篇的 page / ticket / info 是通知通道,不是级别。本文把两者拆开——severity 只表达 P0-P4,通道由路由决定。落在第 8 篇那三档上是这样:P0 → page、P1 → page(IM 强提醒)、P2 → ticket、P3/P4 → info。已经有 severity=page 的存量规则,按这张映射表改一次即可,路由 matcher 跟着换。
2.6 三条铁律
这三条是从多次分级失控里总结出来的,比表格本身更重要:
铁律一:级别只由影响决定,不由因果链决定。
NodeNotReady 是个"原因",Pod 全部 Pending 是"现象",下单失败 是"用户影响"。级别要按最后一个来定。但路由可以按原因来走(节点问题发给平台团队)。定级看结果,路由看原因。
铁律二:每升一级需要额外理由。
从 P2 提到 P1 要写清楚"为什么它够得着叫醒人",从 P1 提到 P0 要写清楚"为什么需要业务负责人在场"。这个理由不用写文档,写在告警规则的 annotation 里就够了——写不出来的,说明它升不了级。
铁律三:级别是路由输入,不是给人看的标签。
如果某个级别没有对应的路由行为(比如 P3 和 P4 都只进看板),那这两个级别就该合并。没有行为差异的级别分级,是自欺欺人。
三、定级判定树:把"感觉"变成"步骤"
框架有了,但落到具体一条告警时,人还是会犹豫。解决办法是把定级过程做成判定树——三步,每步只回答一个是非题。
第 1 步|用户是否真的受影响?
├─ 否 → 走「无影响分支」
│ ├─ 指标在趋势性劣化(容量、证书、连接数)吗?
│ │ ├─ 是 → P3(本周处理)+ 看板 + 周报
│ │ └─ 否 → P4(入库,用于审计与容量统计)
│ └─ 系统能自己可靠自愈吗?
│ ├─ 已验证 → P4 + 自愈事件记录(不发通知)
│ └─ 未验证 → P3(转成"自愈能力待演练"任务)
│
└─ 是 → 进第 2 步
第 2 步|影响是否正在扩大,或者核心链路是否受损?
├─ 核心链路全挂 / 数据丢失或损坏 / 影响正在快速扩大
│ → P0(电话 + 业务负责人在场)
│
├─ 核心链路部分受损 / 非核心全挂 / 单地域单租户受影响
│ → 第 3 步
│
└─ 已有用户抱怨但存在绕过方案,影响面平稳
→ P2(IM,工作时间)
第 3 步|能不能等?
├─ 不能等(正在恶化、没有降级兜底)→ P1(电话或 IM 强提醒,30 分钟内响应)
└─ 能等(已经平稳在坏的水平) → P2(当日处理)3.1 用三个真实场景走一遍
场景 A:支付服务 5xx 占比 12%,持续 4 分钟,还在涨
- 第 1 步:用户受影响?错误率破了 SLO 阈值 → 是
- 第 2 步:核心链路(支付)在快速恶化 → P0
- 动作:电话叫醒值班 + 二线 + 业务负责人,同步在群里发第一条"已确认,正在处理"
场景 B:商品详情页 P99 从 200ms 涨到 800ms,成功率仍 100%,已持续 2 小时不恶化
- 第 1 步:用户受影响?延迟劣化但未破 SLO,成功率无变化 → 边界
- 第 2 步:核心链路部分受损 → 进第 3 步
- 第 3 步:能等吗?已平稳 → P2
- 动作:当日排进 owner 的工单,不响铃
场景 C:某节点 NotReady,该节点上的 8 个 Pod 已重新调度到其他节点,服务无感
- 第 1 步:用户受影响?没有,已自愈且验证过(节点驱逐 + 重新调度跑通过演练)→ 否
- 走无影响分支:可自愈且已验证 → P4
- 动作:只入库,进容量与稳定性周报,供平台团队评估节点健康
同一套步骤,三次走下来结果完全不同。这就是把定级从"经验"变成"流程"的价值:新人也能走出一致的结果,这就是可落地。
3.2 三个常见误判
| 误判 | 表现 | 纠正 |
|---|---|---|
| 按指标定级 | CPU > 90% 就 P1 | 看 SLO 是否真的在被消耗,指标只是线索 |
| 按人熟悉度定级 | 我负责的服务都是 P0 | 级别跟着影响走,不跟着职责走 |
| 按告警名字定级 | 名字里有 Down 就是 P0 | PodDown 单副本且有 5 个副本兜底 = P3 |
四、分级怎么落进规则:把定级标准写进 PromQL
判定树解决了"人怎么想",但落地的关键是把标准写进代码,让机器来定级。这样才不会因为写规则的人不同而级别漂移。
4.1 级别不要和指标塞在同一条规则里
先看一个反例,这是最常见的写法:
# ❌ 反例:一条规则里塞了三个阈值,级别跟着阈值走
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
labels:
severity: warning
- alert: HighErrorRateSevere
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
labels:
severity: critical问题很明显:级别和"5xx 速率"绑死了。这对一个日活 200 的内部报表接口和一个支付接口,严重程度天差地别。而且新增一个级别,就得复制一条规则。
正确的做法是分两层:一层负责算事实(能不能告警),一层负责定级(该是几级)。
4.2 第一层:用 Recording Rule 把 SLO 事实算出来
groups:
- name: slo-facts
interval: 30s
rules:
# 事实 1:当前错误率(分子/分母分离,便于复用)
- record: service:error_ratio:rate5m
expr: |
sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (service) (rate(http_requests_total[5m]))
# 事实 2:这个服务的 SLO 目标(1 - 允许错误率)
# 假设 SLO 定义保存在 service_slo_target 这个外部指标里
# 由 CI 在服务注册时写入,避免规则里硬编码每个服务的 SLO
- record: service:slo_error_budget_ratio
expr: 1 - service_slo_target
# 事实 3:燃烧率 = 当前错误率 / 允许错误率
# 燃烧率 1 = 刚好用完预算;>1 = 超支;14.4 = 1 小时烧掉 2% 的月预算
- record: service:burn_rate:short
expr: service:error_ratio:rate5m
/ on(service) service:slo_error_budget_ratio除了上面三条 Recording Rule,还需要一个由 CI 写入的元数据指标——它不属于 slo-facts 这组规则,但定级阶段会用到:
# CI 在服务注册时,通过 pushgateway 写入服务元数据(值为 1)
cat <<'PROM' | curl --data-binary @- http://pushgateway:9091/metrics/job/service_meta
# TYPE service_meta gauge
service_meta{service="payment-api", tier="core", owner="pay-team", cluster="prod-sh"} 1
service_meta{service="report-job", tier="normal", owner="data-team", cluster="prod-sh"} 1
PROM有了 service_meta,一条定级规则就能覆盖所有服务——新增服务只要注册元数据,不用改任何告警规则。SLO 目标变了,也是改 CI 里的注册信息,不动规则文件。
4.3 第二层:定级只在 label 里做一次
groups:
- name: alert-classification
rules:
# P0:核心链路 + 燃烧率 > 14.4(1 小时烧掉 2% 月预算,快烧)
# service_meta 带 tier label,用它做"核心/非核心"的判定入口
- alert: CoreServiceFastBurn
expr: |
service:burn_rate:short > 14.4
and on(service) (service_meta{tier="core"} == 1)
for: 2m
labels:
severity: P0
page: "true"
annotations:
summary: "核心服务 {{ $labels.service }} 错误预算快速燃烧"
impact: "用户侧错误率 {{ $value | printf \"%.1f\" }} 倍于允许值"
action: "确认影响面 → 回滚最近变更 → 通知业务负责人"
runbook_url: "https://runbook.internal/slo/{{ $labels.service }}"
# P1:核心链路慢烧(3 天烧掉 10%)或非核心链路快烧
- alert: CoreServiceSlowBurnOrNonCoreFastBurn
expr: |
(service:burn_rate:short > 6
and on(service) (service_meta{tier="core"} == 1))
or
(service:burn_rate:short > 14.4
and on(service) (service_meta{tier!="core"} == 1))
for: 15m
labels:
severity: P1
page: "true"
# P2:非核心服务慢烧
- alert: NonCoreServiceSlowBurn
expr: |
service:burn_rate:short > 6
and on(service) (service_meta{tier!="core"} == 1)
for: 30m
labels:
severity: P2
page: "false"
# P3:无告警,仅作为"待观察"事实供看板和周报使用
- record: service:burn_rate:anomaly
expr: service:burn_rate:short > 1.5这套写法的三个关键点:
- 定级规则只有一组,级别差异体现在
expr的组合(tier × 燃烧率),而不是散落在几十条规则里 pagelabel 和severity分离:级别表达"多严重",page表达"要不要响铃"。两者不一定同步——比如"数据备份失败"可能很严重但不需要半夜处理- P3 不产生告警,只产生事实:进看板、进周报,不占据通知通道
4.4 约束:级别必须是枚举,不能是自由文本
这是最容易被忽视、也最容易出事的约束。
# ❌ 这些写法会让路由爆炸
labels: { severity: critical } # 和 P0 是什么关系?
labels: { severity: warning } # 第 6 篇的旧规则,和 P2 怎么映射?
labels: { severity: 高 } # 中文标签,路由匹配不上
labels: { severity: "P0,P1" } # 一条告警两个级别用 CI 做静态检查,在规则合并前拦住:
#!/usr/bin/env bash
# scripts/lint-alert-labels.sh —— 在 CI 里挡住非法级别
# 用法: ./scripts/lint-alert-labels.sh alerts/
set -euo pipefail
DIR="${1:-alerts}"
ALLOWED='^(P0|P1|P2|P3|P4)$'
FAIL=0
# 1) 级别必须是 P0-P4 之一,不允许自由文本/中文/多值
while IFS= read -r hit; do
file="${hit%%:*}"
value="${hit##*severity:}"
value="$(echo "$value" | tr -d ' \t"\r{}[],')"
[ -z "$value" ] && continue
if [[ ! "$value" =~ $ALLOWED ]]; then
echo "❌ $file: 非法级别 '$value',只允许 P0-P4"
FAIL=1
fi
done < <(grep -rno 'severity:[^,}]*' "$DIR" --include='*.yaml' --include='*.yml' || true)
# 2) 含 P0/P1 的规则文件必须有 runbook_url
while IFS= read -r file; do
for lvl in P0 P1; do
if grep -qE "severity:[[:space:]]*\"?$lvl\"?([[:space:]]|$)" "$file" && ! grep -q 'runbook_url' "$file"; then
echo "❌ $file: 含 $lvl 告警但缺少 runbook_url"
FAIL=1
fi
done
done < <(find "$DIR" \( -name '*.yaml' -o -name '*.yml' \) -type f)
exit $FAIL这个脚本故意写得简单粗暴,目的是能塞进现有 CI,不引入额外依赖。真正上线时建议换成基于
promtool输出 JSON 的版本,能拿到完整 AST。
4.5 顺带把 Runbook 也分级
Runbook 是分级的另一个落点:级别决定了 Runbook 里必须写清楚什么。第 8 篇给了完整模板,这里只加一条约束:
| 级别 | Runbook 必备内容 |
|---|---|
| P0 | 影响判定 30 秒版 + 止血优先级 + 回滚命令(可直接复制执行)+ 升级路径与人员名单 |
| P1 | 影响判定 + 止血步骤 + 升级条件 |
| P2 | 排查方向 + 相关看板链接 |
| P3 / P4 | 无需 Runbook,但必须有 owner |
"P0 必须有可复制执行的命令" 这条看着苛刻,但它是有效的:凌晨 3 点没人有精力拼 kubectl 参数。写不出命令的 P0,本质上不配是 P0。
五、路由:让级别真正改变行为
级别如果不改变行为,前面所有工作都是文档游戏。Alertmanager 的路由是级别"起作用"的地方。
# alertmanager.yml —— 按级别路由,与第 8 篇的三级方案兼容
route:
receiver: default-archive # 兜底:级别不明的告警只入库
group_by: ['alertname', 'service', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# P0:电话 + 短信,零延迟,短重发
- matchers: [severity="P0"]
receiver: p0-phone
group_wait: 0s
group_interval: 1m
repeat_interval: 30m
# P1:IM 强提醒(@值班人),10 秒聚合
- matchers: [severity="P1"]
receiver: p1-im-strong
group_wait: 10s
group_interval: 3m
repeat_interval: 2h
# P2:普通 IM,工作时间才发(用 time_intervals 实现)
- matchers: [severity="P2"]
receiver: p2-im
group_wait: 1m
group_interval: 15m
repeat_interval: 8h
active_time_intervals: [business-hours]
# P3:只进看板(webhook 打到看板后端,不发 IM)
- matchers: [severity="P3"]
receiver: dashboard-feed
group_wait: 5m
group_interval: 1h
repeat_interval: 24h
# P4:只入库,不通知
- matchers: [severity="P4"]
receiver: archive-only
time_intervals:
- name: business-hours
time_intervals:
- weekdays: ['monday:friday']
times:
- start_time: '09:00'
end_time: '21:00'5.1 抑制规则:跨级别兜底
抑制的作用,除了第 6 篇讲的"根因抑制症状",还有一个更贴合分级的作用:高级别抑制同源的级别。
inhibit_rules:
# P0 在场时,同一服务的 P1/P2 全部静音(避免噪音淹没有效信息)
- source_matchers: [severity="P0"]
target_matchers: [severity=~"P1|P2"]
equal: ['service', 'cluster']
# P1 在场时,同一服务的 P2/P3 静音
- source_matchers: [severity="P1"]
target_matchers: [severity=~"P2|P3"]
equal: ['service', 'cluster']
# P0/P1 期间,同集群的容量类 P3(磁盘、连接数水位)全部静音
# 理由:正在处理真故障时,容量告警只会干扰判断
- source_matchers: [severity=~"P0|P1"]
target_matchers: [alertname=~".*Capacity.*|.*Watermark.*"]
equal: ['cluster']这条设计有争议——有人担心"抑制会漏掉并发的真问题"。实践中的取舍是:故障期间信息过载的代价,大于并发小问题的漏报代价。但有个前提:被抑制的告警必须仍然记录在案,事后可查(Karma、Thanos 或告警归档里能查到)。
5.2 用 amtool 验证路由,别靠猜
路由配置写对了没,不要等真故障时才知道。
# 逐级验证:每条告警会路由到哪个 receiver
for lvl in P0 P1 P2 P3 P4; do
echo "--- severity=$lvl ---"
amtool --alertmanager.url=http://alertmanager:9093 config routes test \
--verify.receivers \
severity=$lvl service=payment-api cluster=prod-sh
done
# 期望输出:P0 → p0-phone,P1 → p1-im-strong,P2 → p2-im,
# P3 → dashboard-feed,P4 → archive-only
# 如果某条输出 default-archive,说明路由有洞把这几行做进 CI(每次改 alertmanager.yml 都跑一遍),能挡住 90% 的路由事故。这类"配置改错导致半夜没人收到电话"的事故,比想象中常见。
六、让级别不腐化:审计、降级与复盘
分级做完最难的从来不是第一天,而是三个月后:级别只升不降,P0 越来越多,P1 变成了新的 warning。
6.1 三个必须盯着看的指标
这三个指标可以直接用 PromQL 算,做成一张"分级健康度"看板:
# 指标 1:各级别告警量占比(近 30 天)—— P0 占比超过 5% 就该审查
sum by (severity) (
count_over_time(ALERTS{alertstate="firing"}[30d])
)
/ ignoring(severity) group_left
sum(count_over_time(ALERTS{alertstate="firing"}[30d]))
# 指标 2:夜间响铃次数(22:00-08:00)—— 唯一该响的是 P0/P1
sum by (severity) (
increase(alert_notifications_total{time_window="off_hours"}[7d])
)
# 指标 3:级别晋升记录(自有指标,由变更流程写入)
# alert_severity_change_total{alertname, from, to, reason}
sum by (alertname, from, to) (
increase(alert_severity_change_total[90d])
)健康水位参考(30 天窗口,中等规模团队):
| 指标 | 健康 | 警戒 | 说明 |
|---|---|---|---|
| P0 占比 | < 2% | > 5% | P0 应该是罕见事件,不是日常 |
| P0+P1 占比 | < 10% | > 20% | 超过说明分级整体上浮 |
| 夜间响铃次数 | 每周 ≤ 2 次/人 | > 5 次 | 超过就是在消耗团队 |
| 升级/降级比 | 接近 1:1 | 只升不降 | 只升不降 = 没有审计 |
6.2 每季度做一次"级别审计"
具体做什么,四步,两小时能做完:
- 拉出过去 90 天所有 P0,逐条问两个问题:真的需要业务负责人在场吗?如果再来一次,还是 P0 吗?
- 算出每条告警的"命中率":触发次数 vs 真实故障次数。命中率低于 30% 的 P0/P1,强制降级或改造
- 找出从没降过级的告警:90 天内每个级别只升不降,说明定级标准已失效
- 输出一张调整清单:谁、改哪条规则、改成什么级别、理由。放进下个迭代
审计的结果要落到代码里,不能停在会议纪要。落到代码的方式就是提交一次规则变更 PR——review 过程本身就是二次审计。
6.3 分级变更走 Code Review
这是我认为最关键的一条工程约束:
改告警级别 = 改代码 = 走 PR。
把告警规则和 service_meta(tier、owner、SLO 目标)都放进同一个 Git 仓库,级别调整必须:
- 由 owner 提交 PR
- PR 描述里写清"为什么需要升/降级",附上触发记录(触发了几次、其中几次是真故障)
- 至少一位同团队工程师 approve
- CI 跑
promtool check rules+ 级别枚举校验 + 路由连通性测试
好处不只是审计留痕。更重要的是:把"升一级"这个动作的成本,从"改个 yaml 重启一下",提高到"需要解释和 review"。成本提高,随意升级就会减少。
6.4 一次真实的收敛过程
某业务线 6 个月的分级治理轨迹(数字做过脱敏,趋势是真实的):
| 阶段 | 动作 | P0 数量 | P0 占比 | 夜间响铃/周 |
|---|---|---|---|---|
| 起点 | 无分级,全 warning | — | — | 18 |
| 第 1 月 | 引入五级 + 路由,按现有规则硬映射 | 42 条 | 23% | 9 |
| 第 2 月 | 首次审计,P0 砍掉 60%(改为 P1/P2) | 17 条 | 9% | 5 |
| 第 3 月 | 引入 SLO 燃烧率替代阈值规则(第 6 篇方法) | 11 条 | 6% | 3 |
| 第 4-6 月 | 季度审计常态化 + 变更走 PR | 7 条 | 2% | 2 |
值得注意的是第 1 个月:分级刚做完时,P0 数量反而变多了。因为原来所有告警都是 warning,没人区分;一旦设了 P0 这个位置,大家都想往里塞。这个"分级反弹期"几乎必然出现,别被它吓退——它是第 2 个月审计的输入。
真正的拐点在第 4 个月:审计变成常态、变更要走 PR 之后,P0 数量不再反弹。
七、结语:分级是团队对自己注意力的一次预算
写到这里,回到最开始的判断标准:凌晨 3 点,值班工程师醒来知不知道该做什么。
这件事能不能成立,取决于三个东西是否同时存在:
- 一个不会膨胀的级别体系(四维度五级 + 三条铁律 + 判定树)
- 一个能自动执行级别的通道(label 枚举 + 路由 + 抑制 + CI 校验)
- 一个能防止级别腐化的机制(季度审计 + 变更走 PR)
少任何一样,分级都会在 3-6 个月内退化成标签装饰。技术配置是这里面最容易的部分——半天能配完;审计机制和变更纪律才是真正的工作量。
最后给一条能立刻做的起步动作:今天把现有告警按 severity 值导出来,看看有多少种不同的写法。 如果超过 5 种,那你的分级其实还没开始——从统一枚举开始,比重新设计一套五级体系更有效。
附:可直接取用的清单
落地清单(第一周)
- 枚举所有现存
severity值,统一到 P0-P4 - 给每条告警补
ownerlabel(拿不到 owner 的,先降级) - P0/P1 必须有
runbook_url,P0 必须有可复制执行的命令 - 路由配置补齐五级,用
amtool config routes test逐级验证 - 配置 P0 抑制同服务 P1/P2 的抑制规则
常态化清单(每月/每季)
- 每月看一次 P0 占比与夜间响铃次数
- 每季度审计 90 天 P0 清单,输出级别调整 PR
- 命中率 < 30% 的 P0/P1 强制降级或改造
- 检查"只升不降"的告警,重新校准定级标准
本文是 SRE 实战系列第 13 篇。第 3 篇给了 SLI/SLO 与燃烧率的方法论,第 6 篇解决了告警数量问题,第 8 篇讲 on-call 全流程与三级路由,本文补齐了中间那一环——级别从哪来、怎么保证不腐化。四篇连起来,是一套完整的告警体系。
下一篇(第 14 篇):用 eBPF 做网络可观测性——内核挂载点原理、四类信号(流量拓扑 / L7 / TCP / 丢弃)、Hubble 与 Pixie 落地配置、五步排障法。分级解决"叫对人",eBPF 解决"叫了之后 5 分钟内找到证据"。